package ai;

import misc.Constant;
import ai.engine.AIUnit;
import engine.GameEngine;
import engine.UnitAction;
import engine.data.Map;
import engine.data.Unit;
import util.ArrayList;
import util.List;

public class PathFinding {

	public final static byte COMPLETE_PATH = 0; // Get path from start point up to the end point

	public final static byte TYPE_QUICKEST = 0;
	public final static byte TYPE_SHORTEST = 1; // Not Implemented
	public final static byte TYPE_ESCAPE = 2;
	public final static byte TYPE_EXPLORE = 3;

	public static final byte getNextDirection(short type, AIUnit from, Trackable to) {
		return getNextDirection(type, from, to.getX(), to.getY());
	}

	public static final byte getNextDirection(short type, AIUnit from, short[] to) {
		return getNextDirection(type, from, to[0], to[1]);
	}

	public static Path getPath(AIUnit unit, short type, short fromX, short fromY, short targetX, short targetY) {
		switch (type) {
			case TYPE_QUICKEST :
				return getQuickestPath(unit, fromX, fromY, targetX, targetY, GameEngine.map, AIUtil.getCurrentAIPlayer().influenceMapping);
			case TYPE_ESCAPE :
				return getEscapePath(unit, fromX, fromY, targetX, targetY, GameEngine.map, AIUtil.getCurrentAIPlayer().influenceMapping);
			case TYPE_EXPLORE :
				return getExploringPath(unit, fromX, fromY, GameEngine.map, AIUtil.getCurrentAIPlayer().influenceMapping);
			default :
				return new Path();
		}

	}

	private static final byte getNextDirection(short type, AIUnit taskDoer, short targetX, short targetY) {

		Path path = PathFinding.getPath(taskDoer, type, taskDoer.x, taskDoer.y, targetX, targetY);
		if (path.getCoordinatesList().size() != 0) {
			short[] node = (short[]) path.getCoordinatesList().get(0);
			byte direction = getDirection(taskDoer.x, taskDoer.y, node[0], node[1]);
			taskDoer.lastDirection = direction;
			return direction;
		} else
			return UnitAction.SKIP;
	}

	public static byte turnLeft(byte direction) {
		switch (direction) {
			case UnitAction.MOVE_DOWN_RIGHT :
				return UnitAction.MOVE_UP_RIGHT;
			case UnitAction.MOVE_DOWN_LEFT :
				return UnitAction.MOVE_DOWN_RIGHT;
			case UnitAction.MOVE_UP_RIGHT :
				return UnitAction.MOVE_UP_LEFT;
			case UnitAction.MOVE_UP_LEFT :
				return UnitAction.MOVE_DOWN_LEFT;
			case UnitAction.MOVE_RIGHT :
				return UnitAction.MOVE_UP;
			case UnitAction.MOVE_LEFT :
				return UnitAction.MOVE_DOWN;
			case UnitAction.MOVE_DOWN :
				return UnitAction.MOVE_RIGHT;
			case UnitAction.MOVE_UP :
				return UnitAction.MOVE_LEFT;
			default :
				return UnitAction.NONE;
		}
	}

	public static byte turnRight(byte direction) {
		switch (direction) {
			case UnitAction.MOVE_DOWN_RIGHT :
				return UnitAction.MOVE_DOWN_LEFT;
			case UnitAction.MOVE_DOWN_LEFT :
				return UnitAction.MOVE_UP_LEFT;
			case UnitAction.MOVE_UP_RIGHT :
				return UnitAction.MOVE_DOWN_RIGHT;
			case UnitAction.MOVE_UP_LEFT :
				return UnitAction.MOVE_UP_RIGHT;
			case UnitAction.MOVE_RIGHT :
				return UnitAction.MOVE_DOWN;
			case UnitAction.MOVE_LEFT :
				return UnitAction.MOVE_UP;
			case UnitAction.MOVE_DOWN :
				return UnitAction.MOVE_LEFT;
			case UnitAction.MOVE_UP :
				return UnitAction.MOVE_RIGHT;
			default :
				return UnitAction.NONE;
		}
	}

	public static byte getDirection(short currentX, short currentY, short targetX, short targetY) {
		if (currentX < targetX && currentY < targetY)
			return UnitAction.MOVE_DOWN_RIGHT;
		if (currentX > targetX && currentY < targetY)
			return UnitAction.MOVE_DOWN_LEFT;
		if (currentX < targetX && currentY > targetY)
			return UnitAction.MOVE_UP_RIGHT;
		if (currentX > targetX && currentY > targetY)
			return UnitAction.MOVE_UP_LEFT;
		if (currentX < targetX)
			return UnitAction.MOVE_RIGHT;
		if (currentX > targetX)
			return UnitAction.MOVE_LEFT;
		if (currentY < targetY)
			return UnitAction.MOVE_DOWN;
		if (currentY > targetY)
			return UnitAction.MOVE_UP;
		return UnitAction.NONE;
	}

	public static byte getOppositeDirection(byte direction) {
		switch (direction) {
			case UnitAction.MOVE_DOWN_RIGHT :
				return UnitAction.MOVE_UP_LEFT;
			case UnitAction.MOVE_UP_RIGHT :
				return UnitAction.MOVE_DOWN_LEFT;
			case UnitAction.MOVE_DOWN_LEFT :
				return UnitAction.MOVE_UP_RIGHT;
			case UnitAction.MOVE_UP_LEFT :
				return UnitAction.MOVE_DOWN_RIGHT;
			case UnitAction.MOVE_UP :
				return UnitAction.MOVE_DOWN;
			case UnitAction.MOVE_DOWN :
				return UnitAction.MOVE_UP;
			case UnitAction.MOVE_LEFT :
				return UnitAction.MOVE_RIGHT;
			case UnitAction.MOVE_RIGHT :
				return UnitAction.MOVE_LEFT;
			default :
				return UnitAction.NONE;
		}
	}

	public static short[] getPreviousPosition(short x, short y, byte lastDirection) {
		return getFuturePosition(x, y, PathFinding.getOppositeDirection(lastDirection));
	}

	public static short[] getFuturePosition(short x, short y, byte nextDirection) {
		switch (nextDirection) {
			case UnitAction.MOVE_DOWN_RIGHT :
				return new short[] {(short) (x + 1), (short) (y + 1)};
			case UnitAction.MOVE_UP_RIGHT :
				return new short[] {(short) (x + 1), (short) (y - 1)};
			case UnitAction.MOVE_DOWN_LEFT :
				return new short[] {(short) (x - 1), (short) (y + 1)};
			case UnitAction.MOVE_UP_LEFT :
				return new short[] {(short) (x - 1), (short) (y - 1)};
			case UnitAction.MOVE_UP :
				return new short[] {(short) (x), (short) (y - 1)};
			case UnitAction.MOVE_DOWN :
				return new short[] {(short) (x), (short) (y + 1)};
			case UnitAction.MOVE_LEFT :
				return new short[] {(short) (x - 1), (short) (y)};
			case UnitAction.MOVE_RIGHT :
				return new short[] {(short) (x + 1), (short) (y)};
			default :
				return new short[] {(short) (0), (short) (0)};
		}
	}

	public static short getHeuristic(Unit from, Unit to) {
		return getHeuristic(from.x, from.y, to.x, to.y);
	}

	public static short getHeuristic(short currentX, short currentY, short targetX, short targetY) {
		return getDiagonalShortcutHeuristic(currentX, currentY, targetX, targetY);
	}

	private static short getManhattanHeuristic(short currentX, short currentY, short targetX, short targetY) {
		return (short) (10 * (Math.abs(currentX - targetX) + Math.abs(currentY - targetY)));
	}

	private static short getDiagonalShortcutHeuristic(short currentX, short currentY, short targetX, short targetY) {
		short xDistance = (short) (Math.abs(currentX - targetX));
		short yDistance = (short) (Math.abs(currentY - targetY));
		if (xDistance > yDistance)
			return (short) (14 * yDistance + 10 * (xDistance - yDistance));
		else
			return (short) (14 * xDistance + 10 * (yDistance - xDistance));
	}

	private static Path getExploringPath(AIUnit unit, short currentX, short currentY, Map map, byte[][] influenceMapping) {
		Node from = new Node(currentX, currentY);
		Node target = new Node((byte) 0, (byte) 0);
		short[] lastPosition = PathFinding.getPreviousPosition(unit.x, unit.y, unit.lastDirection);
		byte old = influenceMapping[lastPosition[0]][lastPosition[1]];
		influenceMapping[lastPosition[0]][lastPosition[1]] = AIConstant.LAST_POSITION_MALUS;
		Path path = applyAStar(unit, from, target, map, influenceMapping, true, false, COMPLETE_PATH);
		influenceMapping[lastPosition[0]][lastPosition[1]] = old;
		return path;
	}

	private static Path getEscapePath(Unit unit, short currentX, short currentY, short targetX, short targetY, Map map, byte[][] influenceMapping) {
		Node from = new Node(currentX, currentY);
		Node target = new Node(targetX, targetY);
		return applyAStar(unit, from, target, map, influenceMapping, false, true, (byte) 3);
	}

	private static Path getQuickestPath(Unit unit, short currentX, short currentY, short targetX, short targetY, Map map, byte[][] influenceMapping) {
		Node from = new Node(currentX, currentY);
		Node target = new Node(targetX, targetY);
		return applyAStar(unit, from, target, map, influenceMapping, false, false, COMPLETE_PATH);
	}

	private static Path applyAStar(Unit unit, Node start, Node end, Map map, byte[][] influenceMapping, boolean isExploring, boolean isRunningAway, byte pathLength) {
		List open = new ArrayList();
		List closed = new ArrayList();
		start.setGHF(end, map, influenceMapping, isExploring, isRunningAway);
		open.add(start);
		boolean skip = false;
		Node currentNode;
		while (open.size() != 0) {
			AIDebug.logTrace("1- OpenList: " + open);
			AIDebug.logTrace("2- ClosedList: " + closed);
			currentNode = (Node) open.remove(0);
			AIDebug.logTrace("3- CurrentNode: " + currentNode);
			closed.add(currentNode);
			if (isExploring && !map.hasBeenSeen(GameEngine.currPlayer, currentNode.x, currentNode.y))
				return new Path(start, currentNode);
			if (isRunningAway && currentNode.g == pathLength)
				return new Path(start, currentNode);
			if (currentNode.x == end.x && currentNode.y == end.y)
				return new Path(start, currentNode);
			List neighbors = currentNode.getNeighbors();
			while (!neighbors.isEmpty()) {
				skip = false;
				short[] neighbor = (short[]) neighbors.remove(Constant.RANDOM.nextInt(neighbors.size()));
				short cx = neighbor[0];
				short cy = neighbor[1];
				if (isWalkable(unit, cx, cy)) {
					Node n = getNode(open, cx, cy);
					if (n != null && (n.getG(end, currentNode, map, influenceMapping, isExploring, isRunningAway) >= n.g)) {
						skip = true;
					}
					if (!skip) {
						n = getNode(closed, cx, cy);
						if (n != null && (n.getG(end, currentNode, map, influenceMapping, isExploring, isRunningAway) >= n.g))
							skip = true;
					}
					if (!skip) {
						if (n != null) {
							open.remove(n);
							closed.remove(n);
						} else {
							n = new Node(cx, cy);
						}
						n.parent = currentNode;
						n.setGHF(end, map, influenceMapping, isExploring, isRunningAway);
						insertNode(open, n);
					}
				}
			}
		}

		AIDebug.logTrace("No Path Found !");

		return new Path(); // No Path Found
	}

	private static void insertNode(List list, Node node) {
		boolean found = false;
		Node currentNode;
		for (short i = 0; i < list.size() && !found; i++) {
			currentNode = (Node) list.get(i);
			if (node.f <= currentNode.f) {
				list.add(i, node);
				found = true;
				return;
			}
		}
		list.add(node);
	}

	public static boolean isInsideMap(short x, short y) {
		return (x >= 0 && y >= 0 && x < GameEngine.map.width && y < GameEngine.map.height); // TODO
		// Modulo
	}

	private static boolean isWalkable(Unit unit, short x, short y) {
		return (isInsideMap(x, y) && GameEngine.map.getTerrain(x, y).isReachable(unit));
	}

	private static Node getNode(List list, short x, short y) {
		boolean found = false;
		Node currentNode;
		for (short i = 0; i < list.size() && !found; i++) {
			currentNode = (Node) list.get(i);
			if (currentNode.x == x && currentNode.y == y) {
				return currentNode;
			}
		}
		return null;
	}

}
